﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SSharp.Core.Evaluator;

namespace SSharp.Core.DataTypes {
	/// <summary>
	/// A scheme procedure
	/// </summary>
	public sealed class CompoundProcedure : Procedure {
		/// <summary>
		/// The environment in which the procedure was defined
		/// </summary>
		public readonly IEnvironment Environment;

		/// <summary>
		/// The parameters of the procedure.
		/// This can be a list e.g. `(x y), a single varargs parameter e.g. `args or a list followed by a varargs parameter e.g `(x y . args)
		/// </summary>
		public readonly object Parameters;

		/// <summary>
		/// The body of the procedure.
		/// </summary>
		public readonly object Body;

		/// <summary>
		/// Constructs a procedure given its environment, name, parameters and body.
		/// A procedure is immutable.
		/// </summary>
		public CompoundProcedure(IEnvironment environment, string name, object parameters, object body)
			: base (name) {
			ValidateParameters(new HashSet<Symbol>(), parameters);

			Environment = environment;
			Parameters = parameters;
			Body = body;
		}

		/// <summary>
		/// Validates that the parameters are a legal parameter list.
		/// 
		/// A legal parameter list has one of the following forms:
		/// - EmptyList
		/// - A symbol (meaning a varargs)
		/// - A cons of a symbol (the first arg) with a legal parameter list. In this case, the symbol must not be repeated inside the list
		/// </summary>
		private void ValidateParameters(HashSet<Symbol> seenSymbols, object parameters) {
			if (parameters is List.EmptyList) {
				// no more parameters to validate
				return;
			}
			Symbol symbol = parameters as Symbol;
			if (symbol != null) {
				// a varargs symbol; just check that we haven't seen it already
				if (seenSymbols.Contains(symbol)) {
					throw new SyntaxError("Procedure parameters cannot have the same variable name '" + symbol + " twice.");
				}
				// no more parameters to validate
				return;
			}

			Cons cons = parameters as Cons;
			if (cons != null) {
				// the first part must be a symbol; the second part is a legal parameter list
				symbol = cons.car as Symbol;
				if (symbol == null) {
					throw new SyntaxError("Invalid procedure parameter " + cons.car);
				}
				seenSymbols.Add(symbol);
				ValidateParameters(seenSymbols, cons.cdr);
				return;
			}

			// neither a cons nor a symbol
			throw new SyntaxError("Invalid procedure parameter: " + parameters);
		}

		public override Procedure Rename(string name) {
			return new CompoundProcedure(Environment, name, Parameters, Body);
		}

		public override string ToString() {
			return "#<procedure:" + Name + ">";
		}
	}
}
